// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: hgpark

#include "coin2/strategy/rmm_strat/genoi_strategy.h"
#include "coin2/strategy/rmm_strat/util/helper.h"

// TODO(hgpark)
// handle if Binance OI is not ready

namespace coin2::strategy::rmm_strat {

inline void GetNameMultiplier(const std::string& norm, std::optional<double>& multiplier) {
  if (multiplier.has_value()) return;
  *multiplier = 1.0;
  if (norm.find("SHIB") != std::string::npos) {
    std::string base_str = norm.substr(0, norm.find("-USD"));
    if (base_str == "SHIB") {
      *multiplier = 1e3;
    }
  } else if (norm.find("LUNC") != std::string::npos) {
    std::string base_str = norm.substr(0, norm.find("-USD"));
    if (base_str == "LUNC") {
      *multiplier = 1e3;
    }
  }
}

inline std::string GetNormBase(const std::string& base) {
  std::string ret = base;
  if (base.find("SHIB") != std::string::npos) {
    ret = "1000SHIB";
  } else if (base.find("LUNC") != std::string::npos) {
    ret = "1000LUNC";
  }
  return ret;
}

std::string GetProductKey(const IProduct* product) {
  return fmt::format("{}.{}", product->absolute_norm(), product->exchange_name());
}

std::string GetProductKey(const OrderEvent* oe) {
  return fmt::format("{}.{}", oe->symbol(), ExchangeTypeToString(oe->exchange_type()));
}

bool HasMidPrice(const FeedUpdate& upd) {
  bool has_midp = false;
  if (upd.book().Bid0().has_value() &&
      upd.book().Ask0().has_value() &&
      upd.book().Bid0().value().price < upd.book().Ask0().value().price) {
    has_midp = true;
  }
  return has_midp;
}

std::optional<double> GetMidPrice(const FeedUpdate& upd) {
  if (HasMidPrice(upd)) {
    return (upd.book().Bid0().value().price + upd.book().Ask0().value().price) / 2;
  } else {
    return std::nullopt;
  }
}

GenOiStrategy::GenOiStrategy(const std::experimental::filesystem::path& strat_json)
  : strategy_created_time_(GetCurrentTimestamp()) {
  std::ifstream in(strat_json);
  SPDLOG_INFO("Load strategy json: {}", strat_json.string());
  CHECK_THROW(in.is_open());
  auto strat_config_str = ::nlohmann::json::parse(in).dump();
  GenOiStrategyConfig genoi_strat_config;
  auto status = google::protobuf::util::JsonStringToMessage(strat_config_str, &genoi_strat_config);
  CHECK_THROW(status.ok()) << "Fail to parse StratConfig from " << strat_config_str
                           << "\n" << status;
  common_config_ = genoi_strat_config.common_config();
  owner0_ = common_config_.owner0();
  owner1_ = common_config_.owner1();

  RiskManagerConfig risk_config;
  risk_config.set_rest_error_window_size_sec(genoi_strat_config.rest_error_window_size_sec());
  risk_manager_ = std::make_unique<RiskManager>(risk_config);

  for (auto& product_config : genoi_strat_config.product_configs()) {
    genoi_sub_strats_.push_back(std::make_unique<GenOiSubStrategy>(
        this,
        product_config,
        strategy_created_time_));
  }

  for (auto& genoi_sub_strat : genoi_sub_strats_) {
    const auto& key = GetProductKey(genoi_sub_strat->TradeProduct());
    feed_sub_map_[key].push_back(genoi_sub_strat.get());
    CHECK(order_sub_map_.find(key) == order_sub_map_.end());
    order_sub_map_[key] = genoi_sub_strat.get();
    SPDLOG_ERROR("Register {}", key);
    genoi_sub_strat->SetRiskManager(risk_manager_.get());
  }
}

void GenOiStrategy::Init(::coin2::app::Driver* driver) {
  SPDLOG_INFO("GenOi Strategy Init.");
  // TODO(hgpark): Use Strategy Reporter2
  strat_reporter_.reset(new StrategyReporter2(driver->strategy()->config()));
  auto dummy_config = new PassiveExecutorConfig();
  dummy_config->set_lot_size(0);
  dummy_config->set_max_pos(0);
  dummy_config->set_min_pos(0);
  const auto& request0 = driver->order()->GetUniqueSystemByName("PrexGenoi0")->GetAccountRequest();
  const auto& request1 = driver->order()->GetUniqueSystemByName("PrexGenoi1")->GetAccountRequest();

  for (auto& sub_strat : genoi_sub_strats_) {
    sub_strat->Init(driver, strat_reporter_->GetMutableStrategyLogger());
    // strat_reporter_->RegisterProduct(*sub_strat->TradeProduct());
    strat_reporter_->RegisterExecutorConfig(GetCurrentTimestamp(), request0, *sub_strat->TradeProduct(), *dummy_config);
    strat_reporter_->RegisterExecutorConfig(GetCurrentTimestamp(), request1, *sub_strat->TradeProduct(), *dummy_config);
  }
  for (auto& [name, oe] : driver->strategy()->order_executors(0)) {
    (void)name;
    auto mea_ = MarketExchangeApi::FromString(common_config_.trade_product().mea());
    if (oe->mea() == mea_) {
      if (name == owner0_) {
        oe0_ = oe.get();
      } else if (name == owner1_) {
        oe1_ = oe.get();
      }
    }
  }
}

void GenOiStrategy::onBookFeed(const FeedUpdate& upd) {
  auto base = GetNormBase(upd.product().base().currency());
  auto product_key = fmt::format("{}-USD.PERPETUAL.Prex", base);
  CHECK_GT(feed_sub_map_[product_key].size(), 0) << product_key;
  for (auto& sub_strat : feed_sub_map_[product_key]) {
    sub_strat->onBookFeed(product_key, upd);
  }
  if (order_sub_map_.find(product_key) != order_sub_map_.end()) {
    auto midp = GetMidPrice(upd);
    if (midp.has_value()) {
      strat_reporter_->UpdateMidp(*order_sub_map_[product_key]->TradeProduct(), midp);
    }
  }
}

void GenOiStrategy::onOpenInterestFeed(const FeedUpdate& upd) {
  auto base = GetNormBase(upd.product().base().currency());
  auto product_key = fmt::format("{}-USD.PERPETUAL.Prex", base);
  CHECK_GT(feed_sub_map_[product_key].size(), 0);
  for (auto& sub_strat : feed_sub_map_[product_key]) {
    sub_strat->onOpenInterestFeed(upd);
  }
}

void GenOiStrategy::onAccountInfo(const OrderUpdate& upd) {
  int64_t curr_ts = GetCurrentTimestamp();
  if (!oe0_ || !oe1_) {
    SPDLOG_ERROR("oe not ready");
    return;
  }
  if (!upd.is_account_info()) {
    SPDLOG_ERROR("not account info");
    return;
  }
  for (auto& sub_strat : genoi_sub_strats_) {
    sub_strat->onAccountInfo(upd);
  }
  if (strat_reporter_) {
    strat_reporter_->onAccountInfo(upd);
  }
  bool print_log = curr_ts > position_log_ts_ + 10'000'000'000LL;
  for (auto& sub_strat : genoi_sub_strats_) {
    sub_strat->ApproachTargetPosition(
        oe0_,
        oe1_,
        curr_ts,
        print_log);
  }
  if (print_log) {
    position_log_ts_ = curr_ts;
  }
  SPDLOG_INFO_EVERY_NS(10'000'000'000L, curr_ts, oe0_->DebugString(curr_ts));
  SPDLOG_INFO_EVERY_NS(10'000'000'000L, curr_ts, oe1_->DebugString(curr_ts));
}

void GenOiStrategy::onOrderLog(const coin::proto::OrderGatewayLog& order_log) {
  bool orderlog_chosen = (
      order_log.type() == coin::proto::OrderGatewayLog::POSITION
      || order_log.event().type() == OrderEvent::ORDER_FILLED);
  if (orderlog_chosen && strat_reporter_) {
    strat_reporter_->GetMutableStrategyLogger()->WriteOrderLog(
        order_log,
        true,
        GetCurrentTimestamp());
  }
}

void GenOiStrategy::onOrderSubsystemError(const OrderUpdate& upd) {
  SPDLOG_INFO("OrderSubsystemError: {}", coin::proto::OrderUpdateType_Name(upd.update_type()));
  risk_manager_->HandleOrderUpdate(upd);
}

void GenOiStrategy::onCleanup(Driver* driver) {
  std::this_thread::sleep_for(std::chrono::milliseconds(2000));
  for (auto& sub_strat : genoi_sub_strats_) {
    oe0_->CancelOrderAll(*sub_strat->TradeProductHolder());
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    oe1_->CancelOrderAll(*sub_strat->TradeProductHolder());
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }
  SPDLOG_INFO("Strategy start timestamp: {}", strategy_created_time_);
  SPDLOG_INFO("Strategy end timestamp: {}", GetCurrentTimestamp());
  SPDLOG_INFO("Running time: {}s", (GetCurrentTimestamp() - strategy_created_time_) / 1e9);
}

GenOiSubStrategy::GenOiSubStrategy(GenOiStrategy* parent,
                               const GenOiProductConfig& product_config,
                               int64_t strategy_created_time)
  : config_(parent->CommonConfig()),
    strategy_created_time_(strategy_created_time),
    driver_(nullptr),
    strat_logger_(nullptr),
    parent_(parent) {
  config_.MergeFrom(product_config);
  enable_telemetry_ = product_config.enable_telemetry();
  trade_product_ = CreateProductFromNormString(
      MarketExchangeApi::FromString(config_.trade_product().mea()),
      config_.trade_product().norm(),
      GetCurrentTimestamp());
  main_ref_mea_ = MarketExchangeApi::FromString(config_.main_oi_mea_ratio().mea());
  aux_ref_mea_ = MarketExchangeApi::FromString(config_.aux_oi_mea_ratio().mea());
}

void GenOiSubStrategy::Init(
    ::coin2::app::Driver* driver,
    exchange::base::strategy_util::StrategyLogger* strat_logger) {
  driver_ = driver;
  strat_logger_ = strat_logger;
  auto get_product_holder = [this](const coin2::exchange::base::symbology::IProduct& product, int64_t timestamp) {
      return this->driver_->product_holder_manager()->GetProductHolder(product, timestamp);
  };
  trade_product_holder_.reset(get_product_holder(*trade_product_, strategy_created_time_)->Clone());
  feed_keys_.insert(GetProductKey(TradeProduct()));
  if (enable_telemetry_) {
    SPDLOG_INFO("Set Latency Recorder");
    os()->gateway()->set_latency_recorder(strat_logger_->mutable_latency_recorder());
  }
}

void GenOiSubStrategy::onBookFeed(const std::string& product_key, const FeedUpdate& upd) {
  CHECK(feed_keys_.find(product_key) != feed_keys_.end());
  if (upd.mea().exchange == ExchangeType::Prex) {
    if (HasMidPrice(upd)) {
      book_ts_ = upd.book().Timestamp();
      double ask0 = upd.book().Ask0().value().price;
      double bid0 = upd.book().Bid0().value().price;
      midp_ = 0.5 * (ask0 + bid0);
    } else {
      SPDLOG_ERROR("NO MIDP: Prex feed");
    }
  } else {
    SPDLOG_ERROR("Non Prex feed");
  }
}

void GenOiSubStrategy::onAccountInfo(const OrderUpdate& upd) {
  if (!trade_product_holder_) {
    SPDLOG_INFO("product holder not ready");
    return;
  }
  auto& account_info = upd.account_info();
  if (!account_info.is_ready()) {
    SPDLOG_INFO("account info not ready");
    return;
  }
  const auto& prod = *trade_product_;
  if (account_info.HasPosition(prod)) {
    if (account_info.owner() == parent_->Owner0()) {
      position0_ = account_info.GetPosition(prod).net_position();
    } else if (account_info.owner() == parent_->Owner1()) {
      position1_ = account_info.GetPosition(prod).net_position();
    }
  }
}

void GenOiSubStrategy::onOpenInterestFeed(const FeedUpdate& upd) {
  // We assume at least one of main exchange or auxiliary exchange is available
  if (!upd.is_open_interest()) return;
  auto get_product_holder = [this](const coin2::exchange::base::symbology::IProduct& product, int64_t timestamp) {
      return this->driver_->product_holder_manager()->GetProductHolder(product, timestamp);
  };
  if (upd.mea() == main_ref_mea_) {
    if (!main_ref_multiplier_) {
      main_ref_multiplier_ = get_product_holder(upd.product(), upd.timestamp())->product_info().contract_value();
    }
    GetNameMultiplier(upd.product().absolute_norm(), main_ref_name_multiplier_);
    main_oi_ts_ = upd.timestamp();
    main_ref_oi_ = upd.open_interest().open_interest_qty / *main_ref_name_multiplier_ * *main_ref_multiplier_;
  } else if (upd.mea() == aux_ref_mea_) {
    if (!aux_ref_multiplier_) {
      aux_ref_multiplier_ = get_product_holder(upd.product(), upd.timestamp())->product_info().contract_value();
    }
    GetNameMultiplier(upd.product().absolute_norm(), aux_ref_name_multiplier_);
    aux_ref_oi_ = upd.open_interest().open_interest_qty / *aux_ref_name_multiplier_ * *aux_ref_multiplier_;
    if (main_ref_oi_.has_value() && aux_ref_oi_.has_value() &&
        *main_ref_oi_ > 0 && *aux_ref_oi_ > 0 &&
        last_oi_ratio_ts_ != main_oi_ts_) {
      // record (main / aux) oi ratio for main_ref_oi unavailable case
      main_aux_oi_ratio_ = *main_ref_oi_ / *aux_ref_oi_;
      last_oi_ratio_ts_ = main_oi_ts_;
    }
    if (upd.timestamp() - main_oi_ts_ < 1'000'000'000LL * config_.oi_feed_valid_sec()) {
      // Main(Binance) feed is up-to-date
      return;
    }
    SPDLOG_WARN("Main(Binance) {} OI feed is outdated", trade_product_->absolute_norm());
    aux_oi_ts_ = upd.timestamp();
  } else {
    SPDLOG_ERROR("Unkwon mea");
    return;
  }
}

void GenOiSubStrategy::SubmitOrders(
    const double pos,
    const double opposite_pos,
    const double target_pos,
    int64_t* prev_post_ts,
    ::coin2::strategy::order_executor::OrderExecutorSystem* oe,
    int64_t timestamp) {
  int64_t order_dir_int = pos > target_pos ? -1 : 1;
  OrderDirection order_dir, order_opp_dir;
  if (order_dir_int == -1) {
    order_dir = OrderDirection::DIRECTION_SELL;
    order_opp_dir = OrderDirection::DIRECTION_BUY;
  } else if (order_dir_int == 1) {
    order_dir = OrderDirection::DIRECTION_BUY;
    order_opp_dir = OrderDirection::DIRECTION_SELL;
  }
  oe->CancelAllOrders(*trade_product_holder_, order_opp_dir, timestamp);

  auto& tickprc = trade_product_holder_->product_info().tick_price();
  const auto* inside_order = oe->GetInsideOrder(*trade_product_holder_, order_dir);
  double post_price = config_.post_at_midp() ?
    *midp_ : *midp_ * (1 - order_dir_int * config_.post_edge_bps() * 1e-4);

  if (inside_order) {
    oe->CancelAllOrders(*trade_product_holder_, order_dir, timestamp);
    return;
  }
  if (std::abs(pos - target_pos) < std::abs(pos) * 0.01 * config_.oi_sticky_pct()) {
    return;
  }
  if ((pos < target_pos && (pos + opposite_pos) > std::abs(target_pos) * config_.pos_diff_sticky_pct() * 0.01) ||
      (pos > target_pos && (pos + opposite_pos) < -std::abs(target_pos) * config_.pos_diff_sticky_pct() * 0.01)) {
    // avoid single side being filled intensively
    return;
  }
  *prev_post_ts = timestamp;
  double rounded_price = tickprc.RoundPassively(post_price, order_dir);
  double lot_size = std::abs(pos - target_pos) / config_.stack();
  if (lot_size > 0) {
    oe->SubmitOrder(
        *trade_product_holder_,
        rounded_price,
        lot_size,
        order_dir,
        OrderDuration::GTC_ORDER,
        OrderType::LIMIT_ORDER,
        *midp_,
        25, // leverage_rate
        true,  // post_only
        "genoi",
        timestamp,
        false,
        false);
  }
}

void GenOiSubStrategy::ApproachTargetPosition(
    ::coin2::strategy::order_executor::OrderExecutorSystem* oe0,
    ::coin2::strategy::order_executor::OrderExecutorSystem* oe1,
    int64_t timestamp,
    bool print_log) {
  if (!main_ref_oi_.has_value() && !aux_ref_oi_.has_value()) {
    SPDLOG_INFO("Open Interest not ready");
    return;
  }
  if (book_ts_ == 0 || timestamp - book_ts_ > config_.book_feed_valid_sec() * 1e9) {
    SPDLOG_INFO("Book not ready");
    return;
  }
  if (!trade_product_holder_) {
    SPDLOG_INFO("Product holder not ready");
    return;
  }
  if (!oe0->HasProduct(*trade_product_holder_) ||
      !oe1->HasProduct(*trade_product_holder_)) {
    SPDLOG_INFO("Does not have product: {}", trade_product_holder_->product().absolute_norm());
    return;
  }
  if (!position0_ || !position1_) {
    SPDLOG_INFO("Position not ready: {}", trade_product_holder_->product().absolute_norm());
    return;
  }
  double oi_pct = config_.main_oi_mea_ratio().oi_multiplier_pct();
  if (aux_oi_ts_ > main_oi_ts_) {
    // use OI feed from auxiliary reference exchange instead
    if (main_aux_oi_ratio_.has_value()) {
      oi_pct *= *main_aux_oi_ratio_;
    } else {
      SPDLOG_ERROR("No action because Main ref OI feed never arrived!");
      // oi_pct = config_.aux_oi_mea_ratio().oi_multiplier_pct();
      return;
    }
  }
  double open_interest = main_oi_ts_ >= aux_oi_ts_ ? *main_ref_oi_ : *aux_ref_oi_;
  double target_position = open_interest * config_.direction() * oi_pct * 0.01;
  if (print_log) {
    SPDLOG_INFO(
        "Bot0 {}, position: {}, target_position: {}",
        trade_product_->absolute_norm(), *position0_, target_position);
    SPDLOG_INFO(
        "Bot1 {}, position: {}, target_position: {}",
        trade_product_->absolute_norm(), *position1_, -target_position);
  }
  if (timestamp - prev_post_ts0_ < config_.post_cooltime_sec() * 1e9
      || timestamp - prev_post_ts1_ < config_.post_cooltime_sec() * 1e9) {
    return;
  }

  SubmitOrders(*position0_, *position1_, target_position, &prev_post_ts0_, oe0, timestamp);
  SubmitOrders(*position1_, *position0_, -target_position, &prev_post_ts1_, oe1, timestamp);
}

}  // namespace coin2::strategy::rmm_strat

